package com.google.code.UtopiaXml;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Paths;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;

/**
 * Class converts XSD data types to {@link com.google.code.UtopiaXml.Type}
 *
 * @author Jiri Mauritz, Roman Lacko
 */
public class TypeConverter {

    private static Document typemap = null;
    private static XPath xpath = null;
    private static final String DEFAULT_TYPE_NAME = "String";
    private static final SimpleType DEFAULT_TYPE;

    static {
        SimpleTypeReference def = new SimpleTypeReference(DEFAULT_TYPE_NAME);
        def.setDefaultValue("\"\"");
        DEFAULT_TYPE = def;
    }
    // default values
    private static final String TYPEMAP_DEF = "typemap.xml";
    private static final String TYPEMAP_XSD = "typemap.xsd";
    // custom typemap
    private static String TYPEMAP_XML = null;

    private static void checkDocument() throws ParserConfigurationException, SAXException, IOException {
        if (typemap != null) {
            return;
        }

        URL tmapURL;
        if (TYPEMAP_XML == null) {
            tmapURL = TypeConverter.class.getResource(TYPEMAP_DEF);
        } else {
            tmapURL = Paths.get(TYPEMAP_XML).toUri().toURL();
        }

        URL txsdURL = TypeConverter.class.getResource(TYPEMAP_XSD);

        if (tmapURL == null) {
            throw new IOException("Typemap not found");
        }
        if (txsdURL == null) {
            throw new IOException("XML Schema for Typemap could not be located");
        }

        // validation
        try (InputStream mapStream = tmapURL.openStream();
             InputStream xsdStream = txsdURL.openStream()) {

            SchemaFactory schemafactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
            javax.xml.validation.Schema schema = schemafactory.newSchema(new StreamSource(xsdStream));
            Validator validator = schema.newValidator();

            validator.validate(new StreamSource(mapStream));
        }

        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        DocumentBuilder db = dbf.newDocumentBuilder();

        try (InputStream mapStream = tmapURL.openStream()) {
            typemap = db.parse(mapStream);
        }

        XPathFactory xpf = XPathFactory.newInstance();
        xpath = xpf.newXPath();
    }

    private static String cutNameSpace(String fullType) {
        String[] typeArray = fullType.split(":");
        return typeArray[typeArray.length - 1];
    }

    /**
     * Get {@link com.google.code.UtopiaXml.Type} with from XML Schema type name
     *
     * @param schemaType XSD schema type name
     * @return corresponding Type class
     */
    public static SimpleType xmlSchemaToType(String schemaType) {
        try {
            checkDocument();
        } catch (IOException | ParserConfigurationException | SAXException ex) {
            return DEFAULT_TYPE;
        }

        if ((schemaType == null) || (schemaType.isEmpty())) {
            return DEFAULT_TYPE;
        }

        try {
            XPathExpression expr = xpath.compile(String.format("//entry[./@xsd='%s']", cutNameSpace(schemaType)));
            Node node = (Node) expr.evaluate(typemap, XPathConstants.NODE);

            if (node == null) {
                System.err.printf("warning: unknown type '%s', using '%s' instead\n",
                                  schemaType, DEFAULT_TYPE_NAME);
                return DEFAULT_TYPE;
            }

            Node loaderNode = node.getAttributes().getNamedItem("loader");
            Node defvalNode = node.getAttributes().getNamedItem("default");

            String javatype = node.getTextContent();

            SimpleTypeReference stref = loaderNode != null
                                        ? new SimpleTypeReference(javatype, loaderNode.getTextContent())
                                        : new SimpleTypeReference(javatype);

            if (defvalNode != null) {
                stref.setDefaultValue(defvalNode.getTextContent());
            }

            return stref;

        } catch (XPathExpressionException ex) {
            return DEFAULT_TYPE;
        }
    }

    /**
     * Sets the custom typemap and validates it
     *
     * @param fileName typemap file name
     * @throws ParserConfigurationException
     * @throws SAXException
     * @throws IOException
     */
    public static void setTypemap(String fileName) throws
            ParserConfigurationException, SAXException, IOException {
        if (fileName == null) {
            throw new NullPointerException("typemapFN");
        }

        if (typemap != null) {
            throw new IllegalStateException("Typemap is already loaded");
        }

        TYPEMAP_XML = fileName;

        checkDocument();
    }

    /**
     * Returns type that this class uses by default when something goes wrong
     *
     * @return default type
     */
    public static SimpleType getDefaultType() {
        return DEFAULT_TYPE;
    }
}
